谈谈mysql——主从模式下的同步方式及半同步、MGR的部署方式

2023-07-07 17:33:35 292 林溪

MySQL的复制模式

异步复制

MySQL的复制方式默认是异步的,主从复制涉及三个线程

  • master I/O master I/O线程负责写入Binlog,并将执行结果返给客户端,至于Binlog有没有被IO线程读取,读取后有没有重放,重放有没有成功,它是不关心的。
  • slave I/O线程 负责将读取master的Binlog并写入slave的relay log(中继日志)
  • slave sql线程 在slave重放relay log中的event,MySQL5.6以后支持并行复制,所谓并行复制,指的是从库开启多个SQL线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。而MySQL 5.7版本对并行复制进一步改进,已经支持“真正”的并行复制功能,是基于组提交的并行复制,一个组提交的事务都是可以并行回放,官方称为enhanced multi-threaded slave(简称MTS),复制延迟问题已经得到了极大的改进。
在这里插入图片描述

这种模式在高并发的情况下,master只负责写Binlog和返回结果,至于Binlog有没有被读取和重放,它是不关心的,那么这就可能就发生主从延迟的问题。或者master宕机后,slave升级为master,但是binlog未执行完毕,就会导致数据不一致。

这里讲一个小技巧,为了减少主从延迟的问题,主库和备库的硬件性能尽可能保持一致,如果主备硬件差距比较大,从主从延迟的角度上看,应该在性能更好的机器上面安装备库。如果给主库用更好的硬件,那延迟只会越来越大。

半同步复制

为了解决异步存在的问题,MySQL5.5版本推出半同步复制,在master执行完毕后,commit之前,slave的io进程开始把Binlog写入到relaylog中,写完之后,反馈给master,master收到任何一个slave的反馈后,执行commit并返回结果给客户端。半同步的机制减少了主从数据不同步的问题,但是也牺牲了一部分主库的性能,比异步复制至少多了一个tcp的往返时间。

如果在更新期间,主从库发生了网络故障或者从库宕机,那么此时主库在事务提交后会等待10秒(默认值),如果10秒内还是不能联系到该从库,就会放弃更新该从库,并向用户返回数据。这时主从库就会恢复到默认的异步复制状态。

同步复制【不推荐】

全同步复制(full sync replication)是指当主库执行完一个事务后,需要确保所有的从库都执行了该事务才返回给客户端。因为需要等待所有的从库都执行完该事务才能返回,所以全同步复制的性能较差。 MySQL自身不支持同步复制,需要用到第三方工具如DRBD(sync模式)等实现同步复制,严格来说,把半同步复制技术默认(或人为)全部应用到所有从库上也算是全同步复制。

MGR组复制

MGR(MySQL GroupReplication)是MySQL 5.7.17提出的,MGR既可以很好的保证数据一致性又可以自动切换,具备故障检测功能、支持多节点写入.MGR (MySQL Group Replication)是MySQL自带的一个插件,可以灵活部署。MySQL MGR集群是多个MySQL Server节点共同组成的分布式集群,每个Server都有完整的副本,它是基于ROW格式的二进制日志文件和GTID特性。架构主要是APIs层、组件层、复制协议模块层和GCSAPI+Paxos引擎层构成。

MGR的解决的问题
  • 高可用性:由于数据在多个服务器之间复制,因此即使某个服务器出现故障,也不会影响到整体的服务。
  • 数据一致性:MGR使用了一种称为“多数派”或“半数以上”的策略来保证数据的一致性。只有当大多数服务器都确认了一次操作,这次操作才会被视为有效。
  • 负载均衡:MGR允许你将读取操作在多个服务器之间分配,从而实现负载均衡。
MGR带来的问题
  • 写入性能:由于需要在多个服务器之间复制数据,因此写入操作可能会比在单个服务器上执行慢。
  • 网络延迟:如果服务器之间的网络连接不稳定或者延迟高,可能会影响到MGR的性能和可靠性。
  • 管理复杂性:管理和维护一个MGR集群可能会比管理单个服务器更复杂。

如何减少主从延迟的问题

  • 查主库 这是我们目前使用的方法,当要求数据一致性的时候,先写主库,再读主库,比如插入订单表之后要马上查询订单的信息。
  • 分库 原来一个库的并发是400/s,我们把一个库拆分为4个库后,每个库的并发就可以平均到100/s(这个拆分,不是让你把表平均分了,而是要根据业务量拆分,使得每个库都能得到合理的访问量)
  • 硬件设备尽可能保持一致,网络延迟短
  • 开启并行复制。启动多个SQL线程处理relaylog

动手实现半同步和MGR

半同步复制的安装

  • 查看动态加载是否为true 执行命令show variables like "have_dynamic_loading";
  • 安装插件,在master执行INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';,在slave执行INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';
  • 如需卸载(就是关闭半同步),则分别执行UNINSTALL PLUGIN rpl_semi_sync_master;UNINSTALL PLUGIN rpl_semi_sync_slave;
  • 执行SELECT PLUGIN_NAME, PLUGIN_STATUS FROM INFORMATION_SCHEMA.PLUGINS WHERE PLUGIN_NAME LIKE '%semi%';查看插件是否启用
  • MySQL默认是异步的,我们通过命令SET GLOBAL rpl_semi_sync_master_enabled = 1;开启半同步,在slave执行SET GLOBAL rpl_semi_sync_slave_enabled = 1;
  • 也可以配置在my.cnf文件中
#master配置
plugin-load=rpl_semi_sync_master=semisync_master.so
rpl_semi_sync_master_enabled=1

# slave配置
plugin-load=rpl_semi_sync_slave=semisync_slave.so
rpl_semi_sync_slave_enabled=1

  • 重新启动slave的IO thread
mysql> STOP SLAVE IO_THREAD;
Query OK, 0 rows affected, 1 warning (0.03 sec)

mysql> START SLAVE IO_THREAD;
Query OK, 0 rows affected, 1 warning (0.01 sec)
  • 查看半同步的状态,在master执行show status like 'Rpl_semi_sync_master_status';,在slave执行show status like 'Rpl_semi_sync_slave_status';
  • 我们前面说过,超过10s的话半同步就会降级为异步,那么这个10s哪来的呢?我们执行show variables like "rpl_semi_sync_master_timeout";来看一下
  • 我们插入一条记录到user表看一下
  • 接下来我们看看,如果停了slave,master会不会降级为异步。从库执行命令stop slave;主库插入数据,发现执行插入语句后一直等待,10s后返回结果,查看主库的半同步状态show status like 'Rpl_semi_sync_master_status';和从库的状态show status like 'Rpl_semi_sync_slave_status';已经关闭
  • 在slave执行start slave; 半同步状态将会再次开启;
其他参数说明
  • 我们通过show variables like '%Rpl%';show status like '%Rpl_semi%';可以看到其他的参数

  • rpl_semi_sync_master_wait_for_slave_count: MySQL 5.7.3引入的,该变量设置主需要等待多少个slave应答,才能返回给客户端,默认为1。

MGR的相关配置

  • 以下配置是基于MySQL8.0 上篇文章我们使用docker创建了两个MySQL服务,那么在这个案例中,我们还是使用docker快速启动三个服务,在启动前,我们先来看一下需要配置哪些项,我们拿一个配置文件作为参考

#  指定了服务器的唯一标识符,用于复制或群组复制。每个服务器必须有一个不同的server_id值
server_id=1

#  启用了全局事务标识符(GTID)模式,这是一种复制模式,可以跟踪每个事务的执行情况,而不需要使用二进制日志文件名和位置
gtid_mode=ON

#  强制GTID模式的一致性,即只允许那些可以安全地使用GTID复制的语句执行
enforce_gtid_consistency=ON

#  禁用了二进制日志的校验和功能,即不在二进制日志事件中添加额外的数据来验证事件的完整性
binlog_checksum=NONE
 
#  启用了二进制日志,并指定了二进制日志文件的基本名称为binlog
log_bin=binlog

#  使得从服务器在接收到主服务器的更新后,也将这些更新记录到自己的二进制日志中,这对于链式复制或备份从服务器很有用
log_slave_updates=ON

#  指定了二进制日志的格式为行格式,即记录每个被修改的行的变化,而不是记录执行的语句
binlog_format=ROW

#  指定了从服务器存储主服务器信息(如主服务器的主机名、端口号、用户名、密码等)的位置为一个表(mysql.slave_master_info),而不是一个文件(master.info)
master_info_repository=TABLE

#  指定了从服务器存储中继日志信息(如当前正在执行的中继日志文件名和位置等)的位置为一个表(mysql.slave_relay_log_info),而不是一个文件(relay-log.info)
relay_log_info_repository=TABLE
 
#  指定了事务写集提取算法为XXHASH64,这是一种快速的哈希算法,用于生成每个事务写集的唯一标识符,以便在群组复制中检测和避免冲突
transaction_write_set_extraction=XXHASH64

#  指定了群组复制中的群组名称,它是一个128位的无符号整数,用16进制表示,用于标识一个复制群组。所有参与群组复制的服务器必须使用相同的群组名称
loose-group_replication_group_name="aadaaaaa-adda-adda-aaaa-aaaaaaddaaaa"

#  指定了服务器启动时是否自动启动群组复制插件。如果设置为OFF,则需要手动执行START GROUP_REPLICATION语句来启动群组复制
loose-group_replication_start_on_boot=OFF

#  指定了本地服务器在群组复制中使用的地址,包括IP地址和端口号。这个地址用于与其他群组成员通信
loose-group_replication_local_address= "172.17.0.2:13308"

#  指定了一组种子地址,用于在启动群组复制时发现其他群组成员。这些地址可以是本地服务器或远程服务器的地址
loose-group_replication_group_seeds= "172.17.0.2:13308,172.17.0.3:13309,172.17.0.4:13310"

#  指定了是否将本地服务器作为引导服务器,用于创建一个新的群组或恢复一个已有的群组。只有在没有其他可用的群组成员时,才需要设置这个参数为ON
loose-group_replication_bootstrap_group=OFF;

  • 我们创建一个专门的网络,用来连接各个容器
$ docker network create mysql_mgr
$ docker network ls
  • 我们需要MySQL对外服务的端口,还需要群组复制使用的端口,那么我们改一下我们的docker启动命令,我们先来启动一个mysql-c,创建目录mysqlc ,进入目录并执行
docker run -d --name mysql-c -p 23308:3306   \
--net=mysql_mgr \
-v $(pwd)/conf:/etc/mysql/conf.d \
-v $(pwd)/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
mysql

  • 创建目录mysqld并执行
docker run -d --name mysql-d -p 23309:3306   \
--net=mysql_mgr \
-v $(pwd)/conf:/etc/mysql/conf.d \
-v $(pwd)/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
mysql
  • 创建目录mysqle并执行
docker run -d --name mysql-e -p 23310:3306   \
--net=mysql_mgr \
-v $(pwd)/conf:/etc/mysql/conf.d \
-v $(pwd)/data:/var/lib/mysql \
-e MYSQL_ROOT_PASSWORD=root \
mysql
  • 使用下面命令找到容器对应的ip
$ docker inspect -f '{{.Name}} - {{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mysql-c
/mysql-c - 172.17.0.2

  • 我们参照上面的配置文件配置自己my.cnf,并重启docker容器,如果你忘记了my.cnf的目录,可以使用mysql --help | grep "my.cnf"查看my.cnf的目录
[mysqld]
#  指定了服务器的唯一标识符,用于复制或群组复制。每个服务器必须有一个不同的server_id值
server_id=3

#  启用了全局事务标识符(GTID)模式,这是一种复制模式,可以跟踪每个事务的执行情况,而不需要使用二进制日志文件名和位置
gtid_mode=ON

#  强制GTID模式的一致性,即只允许那些可以安全地使用GTID复制的语句执行
enforce_gtid_consistency=ON

#  禁用了二进制日志的校验和功能,即不在二进制日志事件中添加额外的数据来验证事件的完整性
binlog_checksum=NONE
 
#  启用了二进制日志,并指定了二进制日志文件的基本名称为binlog
log_bin=binlog

#  使得从服务器在接收到主服务器的更新后,也将这些更新记录到自己的二进制日志中,这对于链式复制或备份从服务器很有用
log_slave_updates=ON

#  指定了二进制日志的格式为行格式,即记录每个被修改的行的变化,而不是记录执行的语句
binlog_format=ROW

#  指定了从服务器存储主服务器信息(如主服务器的主机名、端口号、用户名、密码等)的位置为一个表(mysql.slave_master_info),而不是一个文件(master.info)
master_info_repository=TABLE

#  指定了从服务器存储中继日志信息(如当前正在执行的中继日志文件名和位置等)的位置为一个表(mysql.slave_relay_log_info),而不是一个文件(relay-log.info)
relay_log_info_repository=TABLE
 
#  指定了事务写集提取算法为XXHASH64,这是一种快速的哈希算法,用于生成每个事务写集的唯一标识符,以便在群组复制中检测和避免冲突
transaction_write_set_extraction=XXHASH64

#  指定了群组复制中的群组名称,它是一个128位的无符号整数,用16进制表示,用于标识一个复制群组。所有参与群组复制的服务器必须使用相同的群组名称
loose-group_replication_group_name="aadaaaaa-adda-adda-aaaa-aaaaaaddaaaa"

#  指定了服务器启动时是否自动启动群组复制插件。如果设置为OFF,则需要手动执行START GROUP_REPLICATION语句来启动群组复制
loose-group_replication_start_on_boot=OFF

#  指定了本地服务器在群组复制中使用的地址,包括IP地址和端口号。这个地址用于与其他群组成员通信
loose-group_replication_local_address= "172.21.0.4:13308"

#  指定了一组种子地址,用于在启动群组复制时发现其他群组成员。这些地址可以是本地服务器或远程服务器的地址
loose-group_replication_group_seeds= "172.21.0.2:13306,172.21.0.3:13307,172.21.0.4:13308"

#  指定了是否将本地服务器作为引导服务器,用于创建一个新的群组或恢复一个已有的群组。只有在没有其他可用的群组成员时,才需要设置这个参数为ON
loose-group_replication_bootstrap_group=OFF;
# 修改MySQL8的验证插件为mysql_native_password
default_authentication_plugin=mysql_native_password
  • 进入容器执行以下命令(每个机器都要操作)
SET SQL_LOG_BIN=0;
CREATE USER mgruser@'%' IDENTIFIED BY 'mgruser';
GRANT REPLICATION SLAVE ON *.* TO mgruser@'%';
FLUSH PRIVILEGES;
SET SQL_LOG_BIN=1;
CHANGE MASTER TO MASTER_USER='mgruser', MASTER_PASSWORD='mgruser' FOR CHANNEL 'group_replication_recovery';
  • 下载插件(每个机器都要操作)
install PLUGIN group_replication SONAME 'group_replication.so';
  • 通过show plugins查看插件是否安装成功
  • 至此,三台MySQL服务器配置完毕,我们现在选择一台(即单主模式)作为master并执行命
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
SELECT * FROM performance_schema.replication_group_members;
  • 因为是mysql8验证方式的问题,我们需要在两台备库上面执行mysql -umgruser -pmgruser -h172.21.0.2 --get-server-public-key
  • 在备库执行
START GROUP_REPLICATION;
  • 查询集群的状态SELECT * FROM performance_schema.replication_group_members; ,全部是online才算成功,中间如果有启动错误,可以通过docker logs container_name来查看错误日志
  • 配置成功后,我们在master新建一个数据库看一下create database mgr;,我们看到从库已经同步过去了;
  • 我们停掉主库看一下stop group_replication;主库和备库的状态,我们发现主库停掉后,会有一个备库升级成主库继续提供服务
在这里插入图片描述
  • 我们在master创建一个表,插入几条记录
use mgr;
CREATE TABLE `user` (
  `id` bigint NOT NULL ,
  `account` varchar(30)  NOT NULL ,
  `name` varchar(50) NOT NULL ,
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `UN_ACCOUNT` (`account`) USING BTREE


INSERT INTO user VALUES (1, 'zhangsan', '张三');
INSERT INTO user VALUES (2, 'lisi', '李四');
在这里插入图片描述
  • 在备库上也插入一条看一下效果,插入失败,并且提示是只读的,而且主库插入的数据也读到了
在这里插入图片描述

参考文档